Skip to content

[Kotlin] Prevent repeated checkout presentation crashes - #600

Open
kieran-osgood-shopify wants to merge 3 commits into
mainfrom
android-duplicate-checkout-crash
Open

[Kotlin] Prevent repeated checkout presentation crashes#600
kieran-osgood-shopify wants to merge 3 commits into
mainfrom
android-duplicate-checkout-crash

Conversation

@kieran-osgood-shopify

@kieran-osgood-shopify kieran-osgood-shopify commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

What changes are you making?

Repeated checkout presentation can fail in two related ways:

  • Multiple present calls while a checkout is visible can stack bottom-sheet windows.
  • Presenting again immediately after dismissal can reuse the detached preloaded WebView. On Android System WebView 133, this reproduced a native Chromium SIGSEGV from a null pointer dereference in libmonochrome_64.so.

This change:

  • Tracks the active bottom-sheet presentation and returns its existing CheckoutHandle when present is called again for the same activity.
  • Clears presentation state and removes the lifecycle observer on every dismissal path.
  • Destroys a presented WebView when its sheet is dismissed instead of retaining it for another presentation. A later presentation creates a fresh WebView.

There is no public API change.

Original stacked-sheet recording (uploaded via Graphite)

How to test

  1. Add an item to the Kotlin sample cart.
  2. Tap Checkout repeatedly while the sheet is opening and verify only one sheet appears.
  3. Dismiss checkout by gesture and immediately tap Checkout again.
  4. Verify the second checkout opens and the app remains alive.

The regression test preloads checkout, presents it, dismisses it, and asserts that the dismissed WebView is destroyed and the next presentation uses a fresh WebView.

Manual verification re-presented checkout 243 ms after gesture dismissal on a Pixel 9 Pro Android 16 emulator. The same app process remained alive and the second checkout rendered.

Validation

  • dev android test
  • dev android lint
  • dev android api check
  • dev android format
  • Clean Kotlin sample build and emulator installation

Before you merge

Important

  • I've added tests to support my implementation
  • I have read and agree with the Contribution Guidelines
  • I have read and agree with the Code of Conduct
  • No README update is required because this does not change public API or documented behavior

kieran-osgood-shopify commented Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

This stack of pull requests is managed by Graphite. Learn more about stacking.

@github-actions github-actions Bot added the #gsd:50662 Rebase Checkout Kit on UCP label Aug 6, 2026
@kieran-osgood-shopify kieran-osgood-shopify changed the title fix(android): ignore a second checkout presentation while one is showing [Kotlin]: Prevent duplicate sheet presentations Aug 7, 2026
@kieran-osgood-shopify kieran-osgood-shopify changed the title [Kotlin]: Prevent duplicate sheet presentations [Kotlin]: Prevent stacked sheet presentations Aug 7, 2026
private val activity: ComponentActivity,
val handle: CheckoutHandle,
) {
fun isShowingFor(context: ComponentActivity): Boolean = activity === context && sheet.isShowing

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wondering here if the activity === context `check is needed?

Could we still stack if presenting from a separate activity? Is that problematic?

fun isShowing(): Boolean = sheet.isShowing

or directly without the function

val alreadyPresented = livePresentation?.takeIf { it.sheet.isShowing }

Maybe with docs to say only one checkout can be presented per SDK process

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good Q - the check gets us closest to the swift implementation, where they drop additional ones on the viewcontroller, it doesn't guard against other viewcontrollers presenting, or the presented via presenting another modal view

I was hoping to avoid a "global" lock situation - what do you think, are you leaning towards a global 1 at a time?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey, yeah it sounds fair to aim for parity with swift

Is there currently a hole with a single livePresentation activity check, where presenting on B overwrites A’s tracked presentation, so a subsequent present on A can still stack a second A sheet.

// maybe we should keep multiple slots
private val livePresentations = mutableMapOf<ComponentActivity, LivePresentation>()
 
// instead of
private var livePresentation: LivePresentation? = null

So we can track active presentations per activity (and return the existing handle only for that activity)?

@kieran-osgood-shopify
kieran-osgood-shopify force-pushed the android-duplicate-checkout-crash branch from d84281e to 99e2e88 Compare August 19, 2026 16:33
@kieran-osgood-shopify kieran-osgood-shopify changed the title [Kotlin]: Prevent stacked sheet presentations [Kotlin] Prevent repeated checkout presentation crashes Aug 19, 2026
### TLDR

`present` built a new `CheckoutBottomSheet` on every call, so N taps on a checkout button stacked N dialog windows. A device capture showed 73 `present` calls producing 73 sheets, two of them only 153 ms apart.

### What

`ShopifyCheckoutKit` now tracks the live presentation. A second `present` for the same activity logs a warning and returns the existing `CheckoutHandle` instead of building another sheet. A stored sheet that is no longer showing counts as stale, so the next `present` proceeds normally.

`CheckoutBottomSheet` gains an internal `onDismissFinalized` callback, invoked in `finishDismiss` above the `isShowing` guard, so every dismissal path clears the slot: buyer dismissal, gesture dismissal, programmatic `dismiss`, lifecycle teardown, and `closeCheckoutWithError`.

That callback also removes the per-presentation `DefaultLifecycleObserver`. Before this change the observer was removed only when `start()` failed, so each successful presentation leaked one observer holding a dead sheet.

No public API change. Both new members are private, or internal on an internal class, so `lib/api/lib.api` does not move.

### How to test

Add an item to the cart in the Kotlin sample, then tap Checkout many times quickly.

| | Before | After |
| --- | --- | --- |
| Sheets shown per burst | 5 | 1 |
| Leaked sheet windows after closing | 4 | 0 |

Leaked sheet windows are `androiddemo` entries in `dumpsys window windows` with `ty=APPLICATION gr=BOTTOM`. Before this change two surfaceless sheet windows survived every close, and one of them still held `mCurrentFocus`. The app then drew nothing, without crashing.

`ShopifyCheckoutKitTest.kt` covers the refusal, the returned handle identity, presenting again after a dismissal, and the observer count returning to its starting value.
@kieran-osgood-shopify
kieran-osgood-shopify force-pushed the android-duplicate-checkout-crash branch from 99e2e88 to 37bce2f Compare September 1, 2026 15:56
@kieran-osgood-shopify
kieran-osgood-shopify marked this pull request as ready for review September 3, 2026 09:32
@kieran-osgood-shopify
kieran-osgood-shopify requested a review from a team as a code owner September 3, 2026 09:32
@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown

Package Size

Platform Artifact Base Head Delta
Android release AAR 364.8 KiB 366.4 KiB +1.6 KiB
Android file breakdown
File Base Head Delta
classes.jar 388.4 KiB 390.3 KiB +1.9 KiB
res/layout/checkout_view_content.xml 2.6 KiB 2.6 KiB 0 B
res/layout/checkout_sheet_content.xml 2.0 KiB 2.0 KiB 0 B
res/values/values.xml 1.3 KiB 1.3 KiB 0 B
R.txt 1.2 KiB 1.2 KiB 0 B
AndroidManifest.xml 922 B 922 B 0 B
proguard.txt 798 B 798 B 0 B
res/drawable/close.xml 431 B 431 B 0 B
res/menu/checkout_menu.xml 354 B 354 B 0 B
META-INF/com/android/build/gradle/aar-metadata.properties 157 B 157 B 0 B

Measured from the PR base SHA and PR head SHA. The file breakdown shows uncompressed sizes within each package artifact, so individual files do not sum to the compressed artifact total. This comment reports package artifact sizes only; it is not a final app binary-size report.

@bitrise

bitrise Bot commented Sep 3, 2026

Copy link
Copy Markdown

Install this build

Open Tophat, select your target device, then click Install. Links open on the Mac running Tophat.

SDK Install
Kotlin Install with Tophat

Checkout Kit E2E results

Status Suite Target Platform OS version tag Device
kotlin-android kotlin android latest Google Pixel 9
Android 17.0

Assisted-By: devx/ae9b9301-933e-49a4-922e-fd6923880697
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

#gsd:50662 Rebase Checkout Kit on UCP

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants